#!/usr/bin/python2.5
# Copyright (C) 2007 Google Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#      http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""
 Editor of GTFS feed.

 This is an extension of schedule_viewer.py, which allows editing
 of stop location usig drag'n'drop.
 You must provide a Google Maps API key.

 Usage:
 schedule_editor.py --key `cat key` --port 8765 --feed_filename feed.zip
"""

import cgi
import re
import urllib
import urlparse

from transitfeed import *

from schedule_viewer import *
from transitfeed_editor import *
import gtfsscheduleeditor

def StopToTuple(stop):
  """Return stop info"""
  return {
           'id': stop.stop_id,
           'name': stop.stop_name,
           'lat': stop.stop_lat,
           'lng': stop.stop_lon,
           'parentStation': stop.parent_station,
           'locationType': stop.location_type
           }

class ScheduleEditorRequestHandler(ScheduleRequestHandler):
  """Schedule with editing function on schedule feeds
  """

  def do_POST(self):
    """Post request dispatcher"""
    scheme, host, path, x, params, fragment = urlparse.urlparse(self.path)
    parsed_params = {}
    # Variable parsed_params is a dict object processed by the following
    # actions to have it encoded in unicode and good enough for the hanlders
    # to be processed smoothly
    for k in params.split('&'):
      k = urllib.unquote(k)
      if '=' in k:
        k, v = k.split('=', 1)
        parsed_params[k] = unicode(v, 'utf8')
      else:
        parsed_params[k] = ''

    if path == '/':
      return self.handle_GET_home()

    m = re.match(r'/post/([a-z]{1,64})', path)

    # Variable path must match the given regex before being dispatched,
    # because the handler is related to the path name.
    if m:
      handler_name = 'handle_POST_%s' % m.group(1)
      handler = getattr(self, handler_name, None)
      if callable(handler):
        form = cgi.FieldStorage(fp=self.rfile,
                                headers=self.headers,
                                environ={'REQUEST_METHOD':'POST',
                                'CONTENT_TYPE':self.headers['Content-Type'],
                                })
        return self.handle_json_wrapper_POST(handler, parsed_params, form)

  def handle_json_wrapper_POST(self, handler, parsed_params, form):
    """Call handler and output the return value in JSON."""
    schedule = self.server.schedule
    result = handler(parsed_params, form)
    content = ResultEncoder().encode(result)
    self.send_response(200)
    self.send_header('Content-Type', 'text/plain')
    self.send_header('Content-Length', str(len(content)))
    self.end_headers()
    self.wfile.write(content)

  def handle_POST_shape(self, params, form):
    """parse shape data and store it into the database"""
    schedule = self.server.schedule
    id = form['shapeId'].value
    shape = schedule.GetShape(id)
    points = form.getlist('points')
    if not shape:
      shape = EditableShape(id)
      schedule.AddGTFSObject(
                             shape,
                             'shape_id',
                             EditableShape._SCHEDULE_REFERENCE,
                             None)
    shape.Update(points, schedule)


  def handle_POST_pattern(self, params, form):
    """Handle pattern related info"""
    act = form['act'].value
    if act == 'updatestoptime':
      return self.updatePatternStop(form)
    if act == 'deletestoptime':
      return self.deletePatternStop(form)

  def deletePatternStop(self, form):
    """Delete pattern stop"""
    schedule = self.server.schedule
    routeId = form['rid'].value
    stopId = form['sid'].value
    sequence = form['seq'].value
    patternId = int(form['pid'].value)
    route = schedule.GetRoute(routeId)
    route.DeleteStopTimeFromPattern(patternId, sequence, stopId, schedule)
    return 1

  def updatePatternStop(self, form):
    """Update pattern stop to new stop"""
    schedule = self.server.schedule
    routeId = form['rid'].value
    sequence = form['seq'].value
    arrivalTime = form['atime'].value
    departureTime = form['dtime'].value
    stopId = form['sid'].value
    patternId = int(form['pid'].value)
    isEnd = int(form['isend'].value)
    route = schedule.GetRoute(routeId)
    route.UpdatePattern(patternId, sequence, arrivalTime, departureTime,
      stopId, isEnd, schedule)
    return 1

  def handle_POST_stop(self, params, form):
    """Parse stop data and store it into the database"""
    act = form['act'].value
    if act == 'save':
      return self.saveStop(form)
    if act == 'copy':
      return self.copyStop(form)
    if act == 'add':
      return self.addStop(form)

  def getStopInsertId(self):
    """Get unique stop id not used"""
    schedule = self.server.schedule
    maxId = 0
    for stop in schedule.GetStopList():
      id = int(stop.stop_id)
      if (id > maxId):
        maxId = id
    maxId += 1
    return maxId

  def addStop(self, form):
    """Add a stop to the shape"""
    schedule = self.server.schedule
    maxId = self.getStopInsertId()
    stop = EditableStop()
    stop.stop_id = "%s" % maxId
    stop.stop_name = form['name'].value.decode('utf-8')
    (lat, lng) = form['latlng'].value.split(',')
    stop.stop_lat = float(lat)
    stop.stop_lon = float(lng)
    stop.Insert(schedule)
    schedule.AddStopObject(stop)
    return [{'id': maxId}]

  def copyStop(self, form):
    schedule = self.server.schedule
    parentId = form['parentId'].value
    parentStop = schedule.GetStop(parentId)
    maxId = self.getStopInsertId()
    stop = EditableStop()
    stop.stop_id = "%s" % maxId
    stop.stop_desc = parentStop.stop_desc
    stop.zone_id = parentStop.zone_id
    stop.stop_url = parentStop.stop_url
    stop.stop_code = parentStop.stop_code
    stop.location_type = 0
    stop.stop_name = form['name'].value.decode('utf-8')
    (lat, lng) = form['latlng'].value.split(',')
    stop.stop_lat = float(lat)
    stop.stop_lon = float(lng)
    if parentStop.parent_station:
      stop.parent_station = parentStop.parent_station
    else:
      stop.parent_station = parentStop.stop_id
      parentStop.ToStation(schedule)
    stop.Insert(schedule)
    schedule.AddStopObject(stop)
    return [{'id': maxId, 'parentStation': stop.parent_station}]

  def saveStop(self, form):
    schedule = self.server.schedule
    id = form['stopId'].value
    stop = schedule.GetStop(id)
    stop.stop_name = form['name'].value.decode('utf-8')
    (lat, lng) = form['latlng'].value.split(',')
    stop.stop_lat = float(lat)
    stop.stop_lon = float(lng)
    stop.Update(schedule);
    return 1

  def AllowEditMode(self):
    """Allow Edit"""
    return True

  def handle_json_GET_agency(self, params):
    """Return a list of all agencies."""
    schedule = self.server.schedule;
    agenciesList = schedule.GetAgencyList();
    agencies = []
    for agency in agenciesList:
      agencies.append({
                      'id': agency.agency_id,
                      'name': agency.agency_name,
                      'url': agency.agency_url,
                      'timezone': agency.agency_timezone,
                      'lang': agency.agency_lang,
                      'phone': agency.agency_phone
                      })
    agencies.sort(key = lambda x: x['name'])
    return agencies

  def handle_json_GET_routesbyaid(self, params):
    """Return a list of routes by agency, aid == "0" for all routes"""
    schedule = self.server.schedule;
    aid = params.get('aid', None)
    routes = []
    if aid is None:
      return routes
    for route in schedule.routes.values():
      if aid == "0" or (('agency_id' in dir(route)) and
                        (aid == route.agency_id)):
        routes.append({
                      'id': route.route_id,
                      'shortName': route.route_short_name,
                      'longName': route.route_long_name,
                      'type': route.route_type,
                      'agencyId': route.agency_id,
                      'desc': route.route_desc,
                      'url': route.route_url,
                      'color': route.route_color,
                      'textColor': route.route_text_color
                      })
    def sort_routes(r1, r2):
      rn1 = r1['longName'] or r1['shortName']
      rn2 = r2['longName'] or r2['shortName']
      return cmp(rn1, rn2)
    routes.sort(sort_routes)
    return routes

  def handle_json_GET_gtfsexport(self, params):
    """Export GTFS Data to a new zip file"""
    schedule = self.server.schedule
    schedule.SaveToFeedFile()
    return [1]

  def handle_json_GET_tripsbyrid(self, params):
    """Return trips of a Route object"""
    schedule = self.server.schedule;
    id = params.get('rid', None)
    route = schedule.GetRoute(id)
    ret = {}
    for trip in route._trips:
      ret.setdefault(trip.pattern_id, []).append({
                 'id': trip.trip_id, 'headsign': trip.trip_headsign,
                 'serviceId': trip.service_id, 'direction': trip.direction_id,
                 'blockId': trip.block_id, 'serviceId': trip.service_id,
                 'shapeId': trip.shape_id, 'shapeDesc': trip.shape_desc,
                 'startTime': trip.start_time
                 })
    return [ret]

  def handle_json_GET_stoptimesbytid(self, params):
    """Return stoptimes of a Trip object"""
    schedule = self.server.schedule
    trip = schedule.GetTrip(params.get('tid'))
    ret = []
    for stoptime in trip.GetStopTimes():
      ret.append({'arrivalTime': stoptime.arrival_time,
                  'departureTime': stoptime.departure_time,
                  'sequence': stoptime.stop_sequence,
                  'stop': {
                           'id': stoptime.stop_id,
                           'name': stoptime.stop.stop_name,
                           'lat': stoptime.stop.stop_lat,
                           'lng': stoptime.stop.stop_lon,
                           'parentStation': stoptime.stop.parent_station,
                           'locationType': stoptime.stop.location_type
                          }
                 })
    return ret

  def handle_json_GET_shapeexist(self, params):
    """Judge the existence of the given shape"""
    schedule = self.server.schedule
    shape = schedule.GetShape(params.get('id'))
    if shape and 'points' in dir(shape):
      if len(shape.points):
        return [1]
    return [0]

  def handle_json_GET_shapesbytid(self, params):
    """return shape points for the given trip id"""
    schedule = self.server.schedule
    trip = schedule.GetTrip(params.get('tid'))
    points = []
    if 'shape_id' in dir(trip):
      shape = schedule.GetShape(trip.shape_id)
      if shape and 'points' in dir(shape):
        for (lat, lon, dist) in shape.points:
          points.append({'lat': lat, 'lng': lon, "dist": dist})
    return points

  def handle_json_GET_boundboxstops(self, params):
    """Return a list of up to 'limit' stops within bounding box with 'n','e'
    and 's','w' in the NE and SW corners. Does not handle boxes crossing
    longitude line 180."""
    schedule = self.server.schedule
    n = float(params.get('n'))
    e = float(params.get('e'))
    s = float(params.get('s'))
    w = float(params.get('w'))
    limit = int(params.get('limit'))
    stops = schedule.GetStopsInBoundingBox(north=n, east=e, south=s, west=w, n=limit)
    return [StopToTuple(s) for s in stops]


def main(RequestHandlerClass = ScheduleEditorRequestHandler):
  usage = (
'''%prog [options] [<input GTFS.zip>]

Runs a webserver that lets you explore a <input GTFS.zip> in your browser.

If <input GTFS.zip> is omited the filename is read from the console.
Dragging a file into the console may enter the filename.
''')
  parser = util.OptionParserLongError(
      usage=usage, version='%prog ' + transitfeed.__version__)
  parser.add_option('--feed_filename', '--feed',
                    dest='feed_filename',
                    help='file name of feed to load')
  parser.add_option('--key', dest='key',
                    help='Google Maps API key or the name '
                    'of a text file that contains an API key')
  parser.add_option('--host', dest='host',
                    help='Host name of Google Maps')
  parser.add_option('--port', dest='port', type='int',
                    help='port on which to listen')
  parser.add_option('--file_dir', dest='file_dir',
                    help='directory containing static files')
  parser.add_option('-n', '--noprompt', action='store_false',
                    dest='manual_entry',
                    help='disable interactive prompts')
  parser.set_defaults(port=8765,
                      host='maps.google.com',
                      file_dir=FindDefaultFileDir(),
                      manual_entry=True)
  (options, args) = parser.parse_args()

  if not os.path.isfile(
      os.path.join(options.file_dir, 'index.html')):
    print ("Can't find index.html with --file_dir=%s" %
      options.file_dir)
    exit(1)

  if not options.feed_filename and len(args) == 1:
    options.feed_filename = args[0]

  if not options.feed_filename and options.manual_entry:
    options.feed_filename = raw_input(
                              'Enter Feed Location: ').strip('"')

  default_key_file = GetDefaultKeyFilePath()
  if not options.key and os.path.isfile(default_key_file):
    options.key = open(default_key_file).read().strip()

  if options.key and os.path.isfile(options.key):
    options.key = open(options.key).read().strip()

  schedule = EditableSchedule(
               options.feed_filename,
               problem_reporter=transitfeed.ProblemReporter())
  print 'Loading data from feed "%s"...' % options.feed_filename
  print '(this may take a few minutes for larger cities)'
  schedule.Load(options.feed_filename)

  server = StoppableHTTPServer(server_address=('', options.port),
                          RequestHandlerClass=RequestHandlerClass)
  server.key = options.key
  server.schedule = schedule
  server.file_dir = options.file_dir
  server.host = options.host
  server.feed_path = options.feed_filename

  print ("To view, point your browser at http://localhost:%d/" %
         (server.server_port))
  server.serve_forever()


def FindPy2ExeBase():
  """If this is running in py2exe return the install directory else return
  None"""
  # py2exe puts gtfsscheduleviewer in library.zip. For py2exe setup.py is
  # configured to put the data next to library.zip.
  windows_ending = gtfsscheduleeditor.__file__.find('\\library.zip\\')
  if windows_ending != -1:
    return transitfeed.__file__[:windows_ending]
  else:
    return None


def FindDefaultFileDir():
  """Return the path of the directory containing the static files. By default
  the directory is called 'files'. The location depends on where setup.py put
  it.
  FindDefaultFileDir is invoked in main function where the parser sets default
  values. It is modified here to set the gtfsscheduleeditor directory as the
  default directory.
  """
  base = FindPy2ExeBase()
  if base:
    return os.path.join(base, 'schedule_editor_files')
  else:
    # For all other distributions 'files' is in the gtfsscheduleviewer
    # directory.
    base = os.path.dirname(gtfsscheduleeditor.__file__)  # Strip __init__.py
    return os.path.join(base, 'files')

if __name__ == '__main__':
  print "starting editor ..."
  main()
